/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.appspot.trafficando.service.aggregator;

import com.appspot.trafficando.domain.SortOrder;
import com.appspot.trafficando.domain.EventAggregator;
import com.appspot.trafficando.domain.Localizer;
import com.appspot.trafficando.domain.EventType;
import com.appspot.trafficando.domain.EventProvider;
import com.appspot.trafficando.util.GeoHelper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.abdera.ext.geo.Point;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.Feed;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import static com.appspot.trafficando.util.AtomHelper.getEmptyFeed;

/**
 *
 * @author Domenico Maria Giffone
 */
public abstract class AbstractEventAggregator implements EventAggregator {

    private final Map<String, EventProvider> providers = new HashMap<String, EventProvider>();
    private final Map<EventType, List<String>> event2Providers = new HashMap<EventType, List<String>>();
    private final Map<String, List<String>> country2Providers = new HashMap<String, List<String>>();
    private Localizer localizer;
    private SortOrder defaultSortOrder;

    @Autowired
    public void setDefaultSortOrder(SortOrder defaultSortOrder) {
        this.defaultSortOrder = defaultSortOrder;
    }

    @Autowired
    public void setLocalizer(Localizer localizer) {
        this.localizer = localizer;
    }

    protected abstract Logger getLogger();

    private List<EventProvider> getProviders(final Set<EventType> eventTypes, String country) {
        List<EventProvider> result = getProvidersByEvent(eventTypes);
        result.retainAll(getProvidersByCountry(country));
        return result;
    }

    protected List<EventProvider> getProviders(final Set<EventType> eventTypes, Point location) {
        return getProviders(eventTypes, localizer.getCountryCode(location));
    }

    protected List<EventProvider> getProvidersByEvent(final Set<EventType> eventTypes) {
        List<EventProvider> result = new ArrayList<EventProvider>();
        for (EventType eventType : eventTypes) {
            if (event2Providers.containsKey(eventType)) {
                result.addAll(getProviders(event2Providers.get(eventType)));
            }
        }
        return result;
    }

    private List<EventProvider> getProvidersByCountry(String country) {
        List<EventProvider> result = new ArrayList<EventProvider>();
        if (country2Providers.containsKey(country)) {
            result.addAll(getProviders(country2Providers.get(country)));
        }
        return result;
    }

    private List<EventProvider> getProviders(final List<String> providerNames) {
        List<EventProvider> result = new ArrayList<EventProvider>();
        for (String name : providerNames) {
            result.add(providers.get(name));
        }
        return result;
    }

    public void addProvider(EventProvider provider) {
        getLogger().info("registering new provider with name: {}", provider.getName());
        this.providers.put(provider.getName(), provider);
        final List<String> supportedCountries = provider.getSupportedCountries();
        final EventType supportedType = provider.getEventType();
        List<String> providerNames;
        for (String country : supportedCountries) {
            providerNames = country2Providers.get(country);
            if (null == providerNames) {
                providerNames = new ArrayList<String>();
                country2Providers.put(country, providerNames);
            }
            getLogger().info("assigning provider with name: {} to country: {}", provider.getName(), country);
            providerNames.add(provider.getName());
        }
        providerNames = event2Providers.get(supportedType);
        if (null == providerNames) {
            providerNames = new ArrayList<String>();
            event2Providers.put(supportedType, providerNames);
        }
        getLogger().info("assigning provider with name: {} to event type: {}", provider.getName(), supportedType);
        providerNames.add(provider.getName());
    }

    protected Feed getFeed(Set<EventType> interests, Point location) {
        Feed result = getEmptyFeed();
        List<EventProvider> selectedProviders = getProviders(interests, location);
        for (EventProvider eventProvider : selectedProviders) {
            List<Entry> entries = eventProvider.getEntries();
            for (Entry entry : entries) {
                Entry newEntry = (Entry) entry.clone();
                result.addEntry(newEntry);
            }
        }
        getLogger().debug("found {} events from {} providers", result.getEntries().size(), selectedProviders.size());
        return result;
    }

    protected void calculateDistance(Feed feed, Point location) {
        for (Entry entry : feed.getEntries()) {
            GeoHelper.addDistance(entry, location);
        }
    }

    protected void sortFeed(Feed feed, SortOrder order) {
        feed.sortEntries(order.getComparator());
    }

    protected Feed filterNearestEvents(Feed feed, Integer distance) {
        getLogger().debug("filtering {} entries", feed.getEntries().size());
        List<Entry> originalEntries = feed.getEntries();
        Feed f = getEmptyFeed();
        for (Entry entry : originalEntries) {
            final Integer entryDistance = Integer.valueOf(GeoHelper.getDistance(entry));
            if (entryDistance.compareTo(distance) <= 0) {
                f.addEntry(entry);
            }
        }
        getLogger().debug("{} entries found within a distance of {}", f.getEntries().size(), distance);
        return f;
    }

    @Override
    public Feed getNearestEvents(Point location, Integer distance, Set<EventType> interests) {
        return getNearestEvents(location, distance, interests, defaultSortOrder);
    }

    @Override
    public Feed getNearestEvents(Point location, Integer distance) {
        return getNearestEvents(location, distance, EventType.ALL);
    }

    @Override
    public Feed getNearestEvents(Point location, Integer distance, Set<EventType> interests, SortOrder order) {
        getLogger().debug("Searching events of type {} within {} KM from {}", new Object[]{interests, distance / 1000, "lat: " + location.getCoordinate().getLatitude() + " lng: " + location.getCoordinate().getLongitude()});
        Feed result = getFeed(interests, location);
        calculateDistance(result, location);
        result = filterNearestEvents(result, distance);
        sortFeed(result, order);
        return result;
    }
}
